<?php
/**
 * Config.php - 9 Aug 2008
 * 
 * Extension of Zend_Config class. Allows config settings to contain other 
 * config settings as value.
 * 
 * When defining a configuration setting, another setting can be used as (a 
 * part of) it's value. To do this the setting to be used must be enclosed by
 * percent signs (%), like %docRoot%. When a setting is inside another section
 * it's name must contain the section name as prefix, followed by a double 
 * collon (::), like %paths::docRoot%. This value will be replaced when the 
 * get() method is called.
 * 
 * So for example, you can create a config file like this:
 * 
 * // ----- EXAMPLE INI CONFIG ----- //
 * 
 * projectName		= someProject
 * 
 * [paths]
 * rootDir 			= /dir/to/root/of/%projectName%
 * 
 * [uploadPaths]
 * uploadDir 		= %paths::rootDir%/upload
 * imgUploadDir		= %uploadDir%/img
 * fileUploadDir	= %uploadDir%/files
 * 
 * 
 * // --- END EXAMPLE INI CONFIG --- //
 * 
 * (ini files can be read using Zend_Config_Ini and converted to an array with
 * the toArray() method, @see Zend_Config_Ini)
 * 
 * @uses Zend_Config
 * 
 * @author Kevin Breed
 */
 
require_once( 'Zend/Config.php' );

class KB_Config extends Zend_Config
{
	/**
	 * @var KB_Config	A reference to the toplevel parent object, so all config
	 * 					settings can be retrieved.
	 */	
	protected $_parent = null;
	
	/**
	 * @var bool	A boolean which indicates whether mergeFirst() is called 
	 * 				before any calls to merge()
	 */
	public static $mergeFirstIsCalled = false;
	
	##############
	# EXCEPTIONS #
	##############
	
	/**
	 * @var	int	Exception code: Method cannot be called.
	 */
	public static $E_IS_PROTECTED = 1;
	
	/**
	 * Constructor, slight modification of parent constructor. An optional 
	 * parent can be given, which is used when the object is merged, to pass a 
	 * reference to the toplevel object, so a config setting can use an other 
	 * setting as value.
	 * 
	 * @param	array		$array				@see Zend_Config::__construct()
	 * @param	bool		$allowModifactions	@see Zend_Config::__construct()
	 * @param	KB_Config	$parent				Reference to the toplevel parent
	 * 											object.
	 */
	public function __construct ( 	$array, 
									$allowModifications = false, 
									$parent = null )
	{
		$this->_allowModifications 	= (boolean) $allowModifications;
        $this->_loadedSection 		= null;
        $this->_index 				= 0;
        $this->_data 				= array();
        $this->_parent				= ( !$parent ) ? $this : $parent; 
        
        foreach ( $array as $key => $value ) 
        {
            if ( is_array( $value ) ) 
            {
            	$this->_data[ $key ] = new self( $value, 
            									 $this->_allowModifications,
            									 $this->_parent );
            } 
            else $this->_data[ $key ] = $value;
        }
        
        $this->_count = count( $this->_data );
	}
	
	/**
	 * Get function that replaces config settings in a config setting with there
	 * value.
	 * 
	 * Config settings are enclosed by percent signs like %setting% when they
	 * are in the same section or in the toplevel category. A section name 
	 * followed by a double collon (::) is needed when a setting from another
	 * section is used. For example %general::settingName% will be replaced with
	 * the value of 'settingName' in section 'general'.
	 * 
	 * @param	string	$name		The name of the config value
	 * @param	mixed	$default	Default value, if the given name can't be 
	 * 								found
	 * @return	mixed				The config value or else the section object
	 */
	public function get ( $name, $default = null )
	{
		$rawValue = parent::get( $name, $default );
		
		if ( is_string( $rawValue) )
		{			
			while ( preg_match( '/%([^% ]*)%/', $rawValue ) > 0 )
			{
				$matches = array();
			
				preg_match_all( '/(%(([^% ]*)::([^% ]*))%)|(%([^% ]*)%)/', 
								$rawValue, 
								$matches, 
								PREG_SET_ORDER );
				
				$patterns 		= array();
				$replacements 	= array();
				
				foreach ( $matches as $match )
				{			
					// no section found
					if ( isset( $match[ 6 ] ) && $match[ 6 ] != '' )
					{
						// search in this object
						$value = parent::get( $match[ 6 ], '' );
						
						// search in parent object
						if ( $value == '' )
							$value = $this->_parent->get( $match[ 6 ], false );
					}
					else
					{
						// search in another section
						$secObj = $this->_parent->get( $match[ 3 ], false );
						
						if ( $secObj ) 	
							$value = $secObj->get( $match[ 4 ], false );
						
						else			
						{
							// search for nested sections
							$nestedSections = explode( '::', $match[ 3 ] );	
						
							if ( count( $nestedSections ) > 0 )
							{
								$firstSec 	= array_shift( $nestedSections );
								$secObj 	= $this->_parent->get( 	$firstSec, 
																	false );
								
								if ( count( $nestedSections ) > 0 && $secObj )
								{
									foreach ( $nestedSections as $nestedSec )
									{
										if ( $secObj )
											$secObj = $secObj->get( $nestedSec,
																	false );
									}
								}
								
								if ( $secObj ) 
									$value = $secObj->get( $match[ 4 ], false );
							}
							else $value = '';	
						}
					}
					
					$patterns[] 		= '/' . $match[ 0 ] . '/';
					$replacements[]		= $value;
				}
				
				$rawValue = preg_replace( $patterns, $replacements, $rawValue );
			}
			
			return $rawValue;
		}
		else return $rawValue;
	}
	
	/**
	 * Basically the same as Zend_Config::merge(), it merges two config objects
	 * together, and assures the extra functionality provided by this class
	 * remains intact. Use this function instead of merge!
	 * 
	 * The given config object will be converted to an instance of this class 
	 * to make sure the extra functionality remains intact. 
	 * 
	 * The $_allowModification property is inherited from the instance on which  
	 * the merge method is called.
	 * 
	 * @param	object		$config	An instance of (a subclass of) the 
	 * 								Zend_Config class.
	 * @return	KB_Config			Returns the merged config object as an
	 * 								instance of this class.
	 */
	public function mergeFirst ( Zend_Config $config )
	{
		self::$mergeFirstIsCalled = true;
		
		// Convert the given config object to an instance of this class (even if
		// it is already an instance of this class), so the $_allowModifications
		// and $_parent property can be reset.
		$kbConfig = new self( 	$config->toArray(), 
								$this->_allowModifications,
								$this );
		
		$merged = $this->merge( $kbConfig );		
		
		self::$mergeFirstIsCalled = false;
		
		return $merged;
	}
	
	/**
	 * Protection for unallowed acces to parent::merge(). Can only be called
	 * by the method mergeFirst().
	 * 
	 * @see mergeFirst()
	 */
	public function merge ( $mergeConf )
	{
		if ( !self::$mergeFirstIsCalled )
		{
			throw new Exception( 'This method is not accessable when using ' .
								 'this class. Use the method mergeFirst() ' .
								 'instead', self::$E_IS_PROTECTED );
			return;
		}
		else 
		{
			foreach( $mergeConf as $key => $item ) 
			{
	            if ( array_key_exists( $key, $this->_data ) ) 
	            {
	                if ( $item instanceof Zend_Config && 
	                	$this->$key instanceof Zend_Config ) 
	                {
	                    $this->$key = $this->$key->merge( $item );
	                } 
	                else 
	                {
	                    $this->$key = $item;
	                }
	            } 
	            else 
	            {
	                $this->$key = $item;
	            }
	        }
	
	        return $this;
		}
	}
	
}

?>